/**
 * © 2009 Susanta Tewari
 * This file is part of genomemap.
 *
 * genomemap is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * genomemap is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with genomemap.  If not, see <http://www.gnu.org/licenses/>.
 */
package genomemap.io;

import genomemap.util.PropertyUtil;
import java.io.IOException;
import java.io.InputStream;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * 
 *
 * Provides an API
 * for the default collection of input files (called, input data collectively). The specification
 * of the default input data is available <a href="doc-files/default_input_data_specs.html">
 * here</a>. A different
 * type of input data could be implemented by extending this class. So, in that respect this class
 * also acts as a base implementation of input data API.
 *
 * <p>
 * A secondary aim is to be a central supplier of IO related settings. For example, a
 * {@link java.text.DecimalFormat.class DecimalFormat} object is provided that should be used
 * throughout.
 * </p>
 *
 * <p>
 * This class implements singleton pattern. An implementing class is loaded via
 * {@link java.util.ServiceLoader.class ServiceLoader} in
 * {@link genomemap.io.DataIO#getInstance() getInstance}.
 * </p>
 *
 *
 * @version 1.0 12/12/2009
 * @author Susanta Tewari
 * 
 */
public class DataIO {

    /**
     * a single instance
     */
    private static DataIO instance;

    /**
     * IO interface of the platform ( desktop, web etc. )
     */
    private PlatformIO platformIOInterface;

    {
        ServiceLoader<PlatformIO> loader = ServiceLoader.load(PlatformIO.class);

        if (!loader.iterator().hasNext())
            throw new RuntimeException("No plugin for genomemap.io.PlatformIOInterface is detected");

        platformIOInterface = loader.iterator().next();

    }

    /**
     * loads properties from the properties file pointed by the system property "io.properties"
     */
    private Properties props = new Properties();

    {
        String propVal = null;
        try {
            propVal = System.getProperty("io.properties");
            props.load(platformIOInterface.getInputStream(propVal));
        } catch (Exception ex) {
            throw new RuntimeException("System property: io.properties = " + propVal);
        }
    }

    /**
     * 
     */
    private List<Integer> linkageGroups;

    /**
     * jar file for the input data
     */
    private JarFile dataJar;

    private DecimalFormat decimalFormat;

    private final String PROP_DECIMAL_FORMAT = "decimal.format";

    private final String PROP_DATA_JAR = "data.jar";

    private final String PROP_LINKAGE_GROUPS = "linkage.groups";

    public static DataIO getInstance() {
        if (instance == null) {
            instance = new DataIO();
        }
        return instance;
    }

    private DataIO() {

        PropertyUtil.validateProperty(PROP_DECIMAL_FORMAT, props.getProperty(PROP_DECIMAL_FORMAT));
        PropertyUtil.validateProperty(PROP_DATA_JAR, props.getProperty(PROP_DATA_JAR));
        PropertyUtil.validateProperty(PROP_LINKAGE_GROUPS, props.getProperty(PROP_LINKAGE_GROUPS));

        // set decimal format
        this.decimalFormat = new DecimalFormat(props.getProperty(PROP_DECIMAL_FORMAT));

        // set data jar
        changeInput(props.getProperty(PROP_DATA_JAR), props.getProperty(PROP_LINKAGE_GROUPS));

    }

    /**
     * Changes input data collection.
     * @param dataJARPropVal replacing the old "data.jar" property value
     * @param linkageGroupsPropVal replacing the old "linkage.groups" property value
     * @throws RuntimeException if error in property values.
     */
    public void changeInput(String dataJARPropVal, String linkageGroupsPropVal) {

        try {

            this.dataJar = new JarFile(dataJARPropVal);

            // linkage groups
            linkageGroups = new ArrayList<Integer>();
            for (String val : linkageGroupsPropVal.split(","))
                linkageGroups.add(Integer.parseInt(val));

            if (linkageGroups.size() == 0)
                throw new RuntimeException("Check property linkage.groups = " + linkageGroupsPropVal);

            // validate data
            validateData();

        } catch (IOException ex) {

            throw new RuntimeException("Property data.jar = " + dataJARPropVal
                    + " does not point to a valid JAR file.");

        } catch (Exception ex) {

            this.dataJar = null;
            this.linkageGroups = null;
        }

        props.setProperty(PROP_DATA_JAR, dataJARPropVal);
        props.setProperty(PROP_LINKAGE_GROUPS, linkageGroupsPropVal);
    }

    // <editor-fold defaultstate="collapsed" desc="Get ch-based Input Streams">
    public InputStream clone_names_file(int linkage_group) {

        return getInputStream(dataJar, "clone_names_" + linkage_group + ".txt");

    }

    public InputStream probe_names_file(int linkage_group) {

        return getInputStream(dataJar, "probe_names_" + linkage_group + ".txt");

    }

    public InputStream gene_names_stream(int linkage_group) {

        return getInputStream(dataJar, "gene_names_" + linkage_group + ".txt");

    }

    public InputStream hybridization_data_file(int linkage_group) {

        return getInputStream(dataJar, "hybridization_data_" + linkage_group + ".txt");

    }

    public InputStream genotype_data_file(int linkage_group) {

        return getInputStream(dataJar, "genotype_data_" + linkage_group + ".txt");

    }// </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Get gw-based Input Streams">
    public InputStream gw_clone_stream() {

        return getInputStream(dataJar, "gw_clones.txt");
    }

    public InputStream gw_ncu_stream() {

        return getInputStream(dataJar, "gw_ncu.txt");
    }

    public InputStream gw_gene_ncu_stream() {

        return getInputStream(dataJar, "gw_gene_ncu.txt");
    }

    public InputStream gw_compl_stream() {

        return getInputStream(dataJar, "gw_compl_data.txt");
    }//</editor-fold>

    /**
     * Provides an implementation of interface <code>PlatformIO</code>
     * @return the default <code>PlatformIO</code> implementation for genomemap.
     */
    public PlatformIO getPlatformIO() {
        return platformIOInterface;
    }

    /**
     * Provides an implementation of <code>DecimalFormat</code>
     * @return he default <code>DecimalFormat</code> implementation for genomemap.
     */
    public DecimalFormat getDecimalFormat() {

        return decimalFormat;

    }

    /**
     * The list of linkage groups to be analyzed.
     * @return a non-empty list of linkage groups to be used in the analysis
     */
    public List<Integer> getLinkageGroups() {
        return linkageGroups;
    }

    /**
     * Gets the input stream for an individual input file from the JAR archive. Note that the
     * archive is checked during construction (using this method) for the presence of necessary
     * files.
     * @param jarFile
     * @param jarEntryFileName
     * @return an input stream for an individual input file from the JAR archive or
     * <code>null</code>.
     */
    private InputStream getInputStream(JarFile jarFile, String jarEntryFileName) {
        try {
            JarEntry entry = jarFile.getJarEntry(jarEntryFileName);
            if (entry == null) {
                return null;
            } else {
                return jarFile.getInputStream(entry);
            }
        } catch (IOException ex) {
            ex.printStackTrace();
            return null;
        }
    }

    /**
     * validates input data and finds the linkage groups for which data is avaialable
     * 1. Check that the following files are present for all chromosomes. In the following i
     *    denotes linkage group.
     *
     *    clone_names_<i>.txt, probe_names_<i>.txt, gene_names_<i>.txt, hybridization_data_<i>.txt
     *    genotype_data_<i>.txt
     *
     *    gw_clones.txt, gw_compl_data.txt, gw_gene_ncu.txt, gw_ncu.txt
     *
     * 2. Validation rule for genome-wide (gw) data.
     *
     *    gw_clones are represented by object CloneSeqInfo
     *     (cloneName:String, contigID:Integer,endPosition:Integer, startPosition:Integer).
     *
     *     All the clones in this set belong to some clones in one or more (repeats) chromosomes.
     *     Issue warning (log message) if a clone of this set is not found in any chromosome.
     *     No duplicate clones.
     *
     *     Property constraints:
     *      cloneName:  any string,
     *      contigID: > 0
     *      startPosition: > 0
     *      endPosition: > 0
     *      startPosition &less endPosition
     *
     *    gw_genes are represented by object GeneSeqInfo
     *     (geneName:String, geneNCUName:String, contigID:Integer, stratPosition:Integer,
     *      endPosition:Integer)
     *
     *      ALl the genes in this set belongs to one and only one chromosome. Issue warning
     *      (log message) if a gene in this set is not found in any chromosome. No duplicate gene
     *      names and NCU names.
     *
     *     Property constraints:
     *      geneName:  any string,
     *      geneNCUName: any string,
     *      contigID: > 0
     *      startPosition: > 0
     *      endPosition: > 0
     *      startPosition &less endPosition
     *
     */
    private void validateData() {

        // gw files
        if (gw_clone_stream() == null)
            throw new RuntimeException("validation error: failed to load. "
                    + "File: gw_clone; Linkage Group: ");


        if (gw_ncu_stream() == null)
            throw new RuntimeException("validation error: failed to load. "
                    + "File: gw_ncu; Linkage Group: ");


        if (gw_gene_ncu_stream() == null)
            throw new RuntimeException("validation error: failed to load. "
                    + "File: gw_gene_ncu; Linkage Group: ");


        if (gw_compl_stream() == null)
            throw new RuntimeException("validation error: failed to load. "
                    + "File: gw_compl; Linkage Group: ");

        // ch files
        for (Integer linkageGroup : linkageGroups) {


            if (clone_names_file(linkageGroup) == null)
                throw new RuntimeException("validation error: failed to load. "
                        + "File: clone_names; Linkage Group: " + linkageGroup);


            if (probe_names_file(linkageGroup) == null)
                throw new RuntimeException("validation error: failed to load. "
                        + "File: probe_names; Linkage Group: " + linkageGroup);


            if (gene_names_stream(linkageGroup) == null)
                throw new RuntimeException("validation error: failed to load. "
                        + "File: gene_names; Linkage Group: " + linkageGroup);


        }


    }

}
